/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package auctions;

import java.util.Arrays;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Random;

/**
 *
 * @author hangsu
 */
public class S2PSB_ScheduleAgent implements ScheduleAgent {
    
    private int agentIdx;
    
    private int bidderNum;
    
    private int slotNum;
    
    private int ddl;
    
    private int needs;
    
    private int currentAuctionIdx;
    
    private double[] utilities;
    
    private BidderResult myResult;
    
    private boolean hasPricePrediction;
    
    private boolean hasPriceDistPrediction;
    
    private double[] pricePrediction;
    
    private double[][] priceDistribution;
    
    private double[] priceExpectation;
    
    private double[][] reversedCDF;
    
    private double confidence;
    
    //private double[] bids;
    
    private S2PSB_strategy strategy;
    
    private HashMap<S2PSB_strategy, double[]> _bidsAllStrategies;
    
    private final ArrayList<AuctionResult> auctionResults;
    
    public enum S2PSB_strategy {
        BASELINE, WEIGHTED, CONF98, CONF90, StraightMV, StraightMU
    }
    
    public S2PSB_ScheduleAgent(int idx, int bidders, int slots, 
            int needs, int ddl, double[] utils)
    {
        this.agentIdx=idx;
        this.bidderNum=bidders;
        this.slotNum=slots;
        this.currentAuctionIdx=0;
        this.myResult=new BidderResult(idx,slots);
        //this.bids=new double[slots];
        this.hasPricePrediction=false;
        this.hasPriceDistPrediction=false;
        this.confidence= GlobalParameters.minConf;
        
        this.resetValue(needs, ddl, utils);
        //Arrays.fill(this.bids, -1);
        this.setStrategy(S2PSB_strategy.BASELINE);
        this.auctionResults = new ArrayList<AuctionResult>();
        this._bidsAllStrategies = new HashMap<S2PSB_strategy, double[]>();
        for(S2PSB_strategy s : S2PSB_strategy.values()) {
            this._bidsAllStrategies.put(s, new double[slots]);
        }
    }
    
    public double getBid() {
        
        /*
         * Since this is simultaneous auction, all bids are calulated 
         * at the first call, later calls to this function simply returns
         * value in the bid table;
         */
        if(this.currentAuctionIdx!=0) {
            return this._bidsAllStrategies.get(this.strategy)[this.currentAuctionIdx++];
        }
        
        double minConfPerSlot = Math.pow(this.confidence, 1.0 / this.needs);
        double[] confOfSlots = new double[this.slotNum];
        double[] minPrices = new double[this.slotNum];
        boolean[] wonIndicator;
        int wonCount;
        double maxProfit = 0;
        double tmpCost;
        int tmpLast;
        
        for(S2PSB_strategy s : S2PSB_strategy.values()) {
            switch (s) {
                case BASELINE: 
                /* 
                 * Baseline strategy without prediction 
                 */
                    Arrays.fill(this._bidsAllStrategies.get(s), 0,this.needs, this.utilities[this.needs-1]/this.needs);
                    Arrays.fill(this._bidsAllStrategies.get(s), this.needs, this.slotNum, -1.0);
                    break;
                case WEIGHTED:
                    double tmp_sum = 0;
                    for(int i=0;i<this.needs;i++)
                        tmp_sum+=Math.pow(2, i);
                    for(int i=0;i<this.needs;i++)
                        this._bidsAllStrategies.get(s)[i]=(Math.pow(2, this.needs-i-1) /tmp_sum)*this.utilities[this.needs-1];
                    Arrays.fill(this._bidsAllStrategies.get(s), this.needs, this.slotNum, -1.0);
                    break;
                case CONF98:
                    this.computeReversedCDF();
                    this.setConf(0.98);
                    minConfPerSlot = Math.pow(this.confidence, 1.0/this.needs);
                    Arrays.fill(confOfSlots, minConfPerSlot);
                    for(int i=0;i<this.slotNum;i++) {
                        int t=(int)(Math.round(Math.ceil(confOfSlots[i]*100)));
                        if(t>99) t=99;
                        minPrices[i]=this.reversedCDF[i][t];
                    }
                    maxProfit=0;
                    Arrays.fill(this._bidsAllStrategies.get(s), 0,this.slotNum, -1);
                    for(int i=0;i<Math.round( Math.pow(2, this.slotNum));i++) {
                        wonCount=0;
                        tmpCost=0;
                        tmpLast=0;
                        wonIndicator=ConvertType.intToBinaryArr(i, this.slotNum);
                        for(int j=0;j<this.slotNum;j++) {
                            if(wonIndicator[j]){
                                wonCount++;
                                tmpCost+=minPrices[j];
                                tmpLast=j;
                            }
                        }
                        if(wonCount>=this.needs ) {
                            if(this.utilities[tmpLast]-tmpCost>maxProfit) {
                                Arrays.fill(this._bidsAllStrategies.get(s), 0,this.slotNum, -1);
                                for(int j=0;j<this.slotNum;j++) {
                                    if(wonIndicator[j])
                                        this._bidsAllStrategies.get(s)[j] = minPrices[j];
                                }
                                maxProfit = this.utilities[tmpLast]-tmpCost;
                            }
                        }
                        
                    }
                    break;
                case CONF90:
                    this.computeReversedCDF();
                    this.setConf(0.9);
                    minConfPerSlot = Math.pow(this.confidence, 1.0/this.needs);
                    maxProfit=0;
                    Arrays.fill(confOfSlots, minConfPerSlot);
                    for(int i=0;i<this.slotNum;i++) {
                        int t=(int)(Math.round(Math.ceil(confOfSlots[i]*100)));
                        if(t>99) t=99;
                        minPrices[i]=this.reversedCDF[i][t];
                    }
                    Arrays.fill(this._bidsAllStrategies.get(s), 0,this.slotNum, -1);
                    for(int i=0;i<Math.round( Math.pow(2, this.slotNum));i++) {
                        wonCount=0;
                        tmpCost=0;
                        tmpLast=0;
                        wonIndicator=ConvertType.intToBinaryArr(i, this.slotNum);
                        for(int j=0;j<this.slotNum;j++) {
                            if(wonIndicator[j]){
                                wonCount++;
                                tmpCost+=minPrices[j];
                                tmpLast=j;
                            }
                        }
                        if(wonCount>=this.needs ) {
                            if(this.utilities[tmpLast]-tmpCost>maxProfit) {
                                Arrays.fill(this._bidsAllStrategies.get(s), 0,this.slotNum, -1);
                                for(int j=0;j<this.slotNum;j++) {
                                    if(wonIndicator[j])
                                        this._bidsAllStrategies.get(s)[j] = minPrices[j];
                                }
                                maxProfit = this.utilities[tmpLast]-tmpCost;
                            }
                        }
                        
                    }
                    break;
                case StraightMV:
                /*
                 * StraightMV
                 */
                    if(!this.hasPricePrediction)
                        this.predictPrice(this.strategy);
                
                    for(int i=0; i<this.slotNum; i++) 
                        this._bidsAllStrategies.get(s)[i] =this.marginalValueOf(i, this.pricePrediction);
                    break;
                case StraightMU:
                /*
                 * StrightMU using price distribution
                 */
                    if(!this.hasPriceDistPrediction)
                        this.predictPriceDist(this.strategy);
                
                    for(int i=0; i<this.slotNum; i++)
                        this._bidsAllStrategies.get(s)[i]=this.marginalValueOf(i, this.pricePrediction);
                    break;
                default:
                    System.err.println("Strategy "+this.strategy.toString()+" not implemented.");
                    break;
            }
        }
        return this._bidsAllStrategies.get(this.strategy)[this.currentAuctionIdx++];

    }

    public void reportResult(AuctionResult lastAuctionResult) {
        
        this.auctionResults.add(lastAuctionResult);
        int curAucIdx = lastAuctionResult.getIdx();
        
        if(lastAuctionResult.getFinalResult().currentWinner==this.agentIdx) { 
            this.myResult.addWinning(curAucIdx, 
                    lastAuctionResult.getFinalResult().currentPrice);
        }
        if(curAucIdx>=this.slotNum-1) { /* last auction */
            int winCount=0;
            int i;
            for(i=0;i<this.slotNum;i++) {
                if(this.myResult.getItemsWon()[i])
                    winCount++;
                if(winCount==this.needs) {
                    this.myResult.setUtil(this.utilities[i]);
                    break;
                }
            }
            if(i>=this.slotNum) /* not fulfill*/
                this.myResult.setUtil(0);
        }
    }
    
    public double marginalValueOf(int slotIdx, double[] prices) {
        double[] prices1 = Arrays.copyOf(prices, this.slotNum);
        double[] prices2 = Arrays.copyOf(prices, this.slotNum);
        prices1[slotIdx]=0;
        prices2[slotIdx]=GlobalParameters.BigInt;
        return this.optimalSurplus(prices1)-this.optimalSurplus(prices2);
    }
    
    public double optimalSurplus(double[] prices) {
        int i=0, j=1;
        while(i<this.slotNum) {
            j*=2;
            i++;
        }
        boolean[] bits;
        int slot, win_count;
        double surplus, max_surplus=0;
        for (i=0; i<j; i++) {
            bits=ConvertType.intToBinaryArr(i, this.slotNum);
            win_count=0;
            surplus=0;
            for(slot=0; slot<this.slotNum; slot++) {
                if(slot>this.ddl) break;
                if(bits[slot]) {
                    win_count++;
                    surplus-=prices[slot];
                    if(win_count>=this.needs) { /* mission completed */
                        surplus+=this.utilities[slot];
                        if(surplus>max_surplus)
                            max_surplus=surplus;
                        break;
                    }
                }
            }
        }
        return max_surplus;
    }

    public void updateStatus(AuctionStatus lastBidStatus) {
        throw new UnsupportedOperationException("Not supported for S2PSB.");
    }

    public BidderResult getBidderResult() {
        return this.myResult;
    }
    
    public void setPricePrediction(double[] prices) {
        this.pricePrediction=Arrays.copyOf(prices, this.slotNum);
        this.hasPricePrediction=true;
    }
    
    public void setPriceDistribution(double[][] dist) {
        this.priceDistribution = dist;
        
        /* also, set point predition as the expectation of distribution */
        int i,j;
        this.priceExpectation = new double[this.slotNum];
        for(i=0;i<this.slotNum;i++) {
            for(j=0;j<=GlobalParameters.MaxPrice;j++)
                this.priceExpectation[i]+=(this.priceDistribution[i][j]*j);
        }
        
        this.hasPriceDistPrediction=true;
    }
    
    public void predictPrice(S2PSB_strategy sty) {
        /* self-confirming prediction */
        System.out.println("[S2PSB]Bidder #"+Integer.toString(this.agentIdx)
                +" is performing self-confirming price point prediction...");
        this.pricePrediction = new double[this.slotNum];
        Arrays.fill(this.pricePrediction, GlobalParameters.PointPriceInit);
        
        int N=this.bidderNum;
        int M=this.slotNum;
        Random r=new Random();
        ArrayList<ScheduleAgent> agents = new ArrayList<ScheduleAgent>();
        int i, j, k;
        for(i=0; i<N; i++) {
            S2PSB_ScheduleAgent agent = new S2PSB_ScheduleAgent(i, N, M, 0, 0, null);
            agent.setStrategy(sty);
            agents.add(agent);
        }
        ScheduleAuctionhouse house = new ScheduleSimultaneousAuctionHouse(M, AuctionType.SEALEDBID);
        house.setAgents(agents);
        
        /*price accumulation in iteration*/
        double[] accum_price = new double[this.slotNum];
        /*decaying sequence*/
        double[] decay = new double[GlobalParameters.MaxIteration];
        for(i=0; i<decay.length; i++)
            decay[i]=1-(double)i/decay.length;
        
        int lamda;
        double[] utils = new double[M];
        double tmp_diff, tmp_max;
        for (int iter=0; iter<GlobalParameters.MaxIteration; iter++) {
            System.out.print("Iter "+Integer.toString(iter));
            /*TODO reset random seed here?*/
            /*run multiple random instances*/
            Arrays.fill(accum_price, 0); /*reset accumulation*/
            for(i=0; i<GlobalParameters.NumInstances; i++) {
                house.clear();
                for(j=0; j<N; j++) {
                    lamda = r.nextInt(M)+1;
                    for (k=0; k<M; k++)
                        utils[k]=(double)r.nextInt(GlobalParameters.MaxPrice)+1;
                    Arrays.sort(utils);
                    for(k=0;k<M/2;k++) {
                        double tmp_d = utils[k];
                        utils[k]=utils[M-1-k];
                        utils[M-1-k]=tmp_d;
                    }
                    agents.get(j).resetValue(lamda, M-1, utils);
                    ((S2PSB_ScheduleAgent)(agents.get(j))).setPricePrediction(this.pricePrediction);
                }
                house.startAuctions();
                
                for(j=0;j<this.slotNum;j++) 
                    accum_price[j]+=house.getAuctionsResults().get(j).getFinalResult().currentPrice;
            }
            tmp_max=0;
            for(j=0;j<this.slotNum;j++) {
                /*average from instance prices*/
                accum_price[j]/=GlobalParameters.NumInstances;
                /*new price*/
                accum_price[j]=this.pricePrediction[j]
                        +decay[iter]*(accum_price[j]-this.pricePrediction[j]);
                /*calc diff*/
                tmp_diff=Math.abs(accum_price[j]-this.pricePrediction[j]);
                if(tmp_diff>tmp_max)
                    tmp_max=tmp_diff;
                /*set new price*/
                this.pricePrediction[j]=accum_price[j];
            }
            System.out.println(" "+Double.toString(tmp_max));
            
            if(tmp_max<=GlobalParameters.ConvergeThres)
                break;
        }
        /*
        System.out.println("");
        for(i=0;i<this.slotNum;i++)
            System.out.print(Double.toString(this.pricePrediction[i])+" ");
        System.out.println("");
         * 
         */
        
        
        this.hasPricePrediction=true;
    }
    
    public void predictPriceDist(S2PSB_strategy sty) {
        /* self-confirming prediction */
        System.out.println("[S2PSB]Bidder #"+Integer.toString(this.agentIdx)
                +" is performing self-confirming price distribution prediction...");
        int N=this.bidderNum;
        int M=this.slotNum;
        int i,j,k;
        this.priceDistribution = new double[M][GlobalParameters.MaxPrice+1]; 
        for(i=0; i<M; i++)
            Arrays.fill(this.priceDistribution[i], GlobalParameters.DistPriceInit);

        Random r=new Random();
        ArrayList<ScheduleAgent> agents = new ArrayList<ScheduleAgent>();
        for(i=0; i<N; i++) {
            S2PSB_ScheduleAgent agent = new S2PSB_ScheduleAgent(i, N, M, 0, 0, null);
            agent.setStrategy(sty);
            agents.add(agent);
        }
        ScheduleAuctionhouse house = new ScheduleSimultaneousAuctionHouse(M, AuctionType.SEALEDBID);
        house.setAgents(agents);
        
        /*distribution in iteration*/
        double[][] accum_dist = new double[M][GlobalParameters.MaxPrice+1];
        
        /*decaying sequence*/
        double[] decay = new double[GlobalParameters.MaxIteration];
        for(i=0; i<decay.length; i++)
            decay[i]=1-(double)i/decay.length;
        
        int lamda;
        double[] utils = new double[M];
        double tmp_diff, tmp_max;
        for (int iter=0; iter<GlobalParameters.MaxIteration; iter++) {
            System.out.print("Iter "+Integer.toString(iter));
            /*TODO reset random seed here?*/
            
            /*
             * run multiple random instances
             */
            
            /*reset accumulation*/
            for(i=0; i<M; i++) 
                Arrays.fill(accum_dist[i], 0);
            for(i=0; i<GlobalParameters.NumInstances; i++) {
                house.clear();
                for(j=0; j<N; j++) {
                    lamda = r.nextInt(M)+1;
                    for (k=0; k<M; k++)
                        utils[k]=(double)r.nextInt(GlobalParameters.MaxPrice)+1;
                    Arrays.sort(utils);
                    for(k=0;k<M/2;k++) {
                        double tmp_d = utils[k];
                        utils[k]=utils[M-1-k];
                        utils[M-1-k]=tmp_d;
                    }
                    agents.get(j).resetValue(lamda, M-1, utils);
                    ((S2PSB_ScheduleAgent)(agents.get(j))).setPriceDistribution(this.priceDistribution);
                }
                house.startAuctions();
                
                for(j=0;j<this.slotNum;j++) {
                    int roundedPrice=(int)Math.round(house.getAuctionsResults().get(j).getFinalResult().currentPrice);
                    if(roundedPrice>GlobalParameters.MaxPrice || roundedPrice<0) {
                        System.err.println("Price out of bound.");
                        continue;
                    }
                    accum_dist[j][roundedPrice]+=1.0;
                }
            }
            tmp_max=0;
            for(j=0;j<this.slotNum;j++) {
                for(k=0;k<=GlobalParameters.MaxPrice;k++) {
                    /*normalize instance prices*/
                    accum_dist[j][k]/=GlobalParameters.NumInstances;
                    /*new price*/
                    accum_dist[j][k] = this.priceDistribution[j][k]
                            +decay[iter]*(accum_dist[j][k]-this.priceDistribution[j][k]);
                    /*calc diff*/
                    tmp_diff=Math.abs(accum_dist[j][k]-this.priceDistribution[j][k]);
                    if(tmp_diff>tmp_max)
                        tmp_max=tmp_diff;
                    /*set new price*/
                    this.priceDistribution[j][k]=accum_dist[j][k];
                }
            }
            System.out.println(" "+Double.toString(tmp_max));
            
            if(tmp_max<=GlobalParameters.ConvergeKSThres)
                break;
        }
        
        /* also, set point predition as the expectation of distribution */
        this.priceExpectation = new double[this.slotNum];
        for(i=0;i<this.slotNum;i++) {
            for(j=0;j<=GlobalParameters.MaxPrice;j++)
                this.priceExpectation[i]+=(this.priceDistribution[i][j]*j);
        }
        
        this.hasPriceDistPrediction=true;
        /*
         * Display distribution
        System.out.println("{");
        for(i=0;i<this.slotNum;i++) {
            System.out.print("\t{");
            for(j=0;j<=GlobalParameters.MaxPrice;j++) {
                if(j!=0) System.out.print(", ");
                System.out.print(Double.toString(this.priceDistribution[i][j]));
                if(j%5==4)
                    System.out.print("\n\t");
            }
            System.out.print("}, \n");
        }
        System.out.println("}");
        
         * 
         * 
         */
         
    }
    
    public void computeReversedCDF() {
        /* discrete into 100 bins */
        this.reversedCDF = new double[this.slotNum][100];
        assert(this.hasPriceDistPrediction);
        int slotidx, i, currPrice;
        double currCDF;
        for(slotidx=0;slotidx<this.slotNum;slotidx++) {
            currPrice=50;
            currCDF=1;
            for(i=99; i>=0; i--) {
                while(currCDF>0.01*i) {
                    currCDF-=this.priceDistribution[slotidx][currPrice--];
                    if(currPrice<0)
                        break;
                }
                this.reversedCDF[slotidx][i]=currPrice+1;
                if(currPrice<0) break;
            }
        }
    }
    
    public final void setStrategy(S2PSB_strategy s) {
        this.strategy=s;
    }
    
    public final S2PSB_strategy getStrategy() {
        return this.strategy;
    }
        
    public final void resetValue(int needs, int ddl, double[] utils) {
        this.ddl=ddl;
        this.needs=needs;
        if(utils!=null)
            this.utilities=Arrays.copyOf(utils, this.slotNum);
    }
    
    public double computeRegret() {
        double maxPossibleProfit=this.myResult.getProfit();
        S2PSB_strategy maxPossibleStrategy = this.strategy;
        
        double profit;
        double cost;
        int i, j;
        ArrayList<Double> bidHistory;
        int maxBidder;
        double maxBid;
        boolean[] won = new boolean[this.slotNum];
        int wonCount;
        
        for(S2PSB_strategy s : S2PSB_strategy.values()) {
            if(s.compareTo(this.strategy) == 0 ) continue;
            profit=0;
            cost=0;
            Arrays.fill(won, false);
            for(i = 0; i<this.slotNum; i++) {
                bidHistory = this.auctionResults.get(i).getBidsOfRound(0);
                maxBidder=-1;
                maxBid=new Double (0);
                for(j = 0; j<this.bidderNum; j++) {
                    if(j!=this.agentIdx) {
                        /* NOTE: can Double compare with double ?? */
                        if(bidHistory.get(j).compareTo(maxBid) > 0) {
                            maxBid = bidHistory.get(j);
                            maxBidder = j;
                        }
                    } else {
                        if(this._bidsAllStrategies.get(s)[i]>maxBid) {
                            maxBid = this._bidsAllStrategies.get(s)[i];
                            maxBidder = this.agentIdx;
                        }
                    }
                }
                if(maxBidder == this.agentIdx) {
                    cost+=maxBid;
                    won[i]=true;
                }
            }
            /* count won slots */
            wonCount = 0;
            for(i=0;i<this.slotNum;i++) {
                if(won[i]) wonCount++;
                if(wonCount>=this.needs) {
                    profit = this.utilities[i]-cost;
                    break;
                }
            }
            if(i>=this.slotNum) profit = 0 - cost;
            if(profit>maxPossibleProfit) {
                maxPossibleProfit = profit;
                maxPossibleStrategy = s;
            }
        }
        return maxPossibleProfit-this.myResult.getProfit();
    }
    
    public void setConf(double conf) {
        this.confidence = conf;
    }

    public void clear() {
        this.currentAuctionIdx = 0;
        this.myResult.clear();
        //Arrays.fill(this.bids, -1);
        /* NOTE: prediction is not reset here for convenience */
        //this.hasPricePrediction=false;
        /* TODO may need more codes after adding more fields */
    }
    
}
